package cn.note.swing.core.component;

import javax.swing.*;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.*;

/**
 * A collection of utility methods for Swing.
 *
 * @author Darryl Burke
 */
public final class SwingUtils {

    private SwingUtils() {
        throw new Error("SwingUtils is just a container for static methods");
    }

    /**
     * Convenience method for searching below <code>container</code> in the
     * component hierarchy and return nested components that are instances of
     * class <code>clazz</code> it finds. Returns an empty list if no such
     * components exist in the container.
     * <P>
     * Invoking this method with a class parameter of JComponent.class
     * will return all nested components.
     * <P>
     * This method invokes getDescendantsOfType(clazz, container, true)
     *
     * @param clazz the class of components whose instances are to be found.
     * @param container the container at which to begin the search
     * @return the List of components
     */
    public static <T extends JComponent> List<T> getDescendantsOfType(
            Class<T> clazz, Container container) {
        return getDescendantsOfType(clazz, container, true);
    }

    /**
     * Convenience method for searching below <code>container</code> in the
     * component hierarchy and return nested components that are instances of
     * class <code>clazz</code> it finds. Returns an empty list if no such
     * components exist in the container.
     * <P>
     * Invoking this method with a class parameter of JComponent.class
     * will return all nested components.
     *
     * @param clazz the class of components whose instances are to be found.
     * @param container the container at which to begin the search
     * @param nested true to list components nested within another listed
     * component, false otherwise
     * @return the List of components
     */
    public static <T extends JComponent> List<T> getDescendantsOfType(
            Class<T> clazz, Container container, boolean nested) {
        List<T> tList = new ArrayList<T>();
        for (Component component : container.getComponents()) {
            if (clazz.isAssignableFrom(component.getClass())) {
                tList.add(clazz.cast(component));
            }
            if (nested || !clazz.isAssignableFrom(component.getClass())) {
                tList.addAll(SwingUtils.<T>getDescendantsOfType(clazz,
                        (Container) component, nested));
            }
        }
        return tList;
    }

    /**
     * Convenience method that searches below <code>container</code> in the
     * component hierarchy and returns the first found component that is an
     * instance of class <code>clazz</code> having the bound property value.
     * Returns {@code null} if such component cannot be found.
     * <P>
     * This method invokes getDescendantOfType(clazz, container, property, value,
     * true)
     *
     * @param clazz the class of component whose instance is to be found.
     * @param container the container at which to begin the search
     * @param property the className of the bound property, exactly as expressed in
     * the accessor e.g. "Text" for getText(), "Value" for getValue().
     * @param value the value of the bound property
     * @return the component, or null if no such component exists in the
     * container
     * @throws java.lang.IllegalArgumentException if the bound property does
     * not exist for the class or cannot be accessed
     */
    public static <T extends JComponent> T getDescendantOfType(
            Class<T> clazz, Container container, String property, Object value)
            throws IllegalArgumentException {
        return getDescendantOfType(clazz, container, property, value, true);
    }

    /**
     * Convenience method that searches below <code>container</code> in the
     * component hierarchy and returns the first found component that is an
     * instance of class <code>clazz</code> and has the bound property value.
     * Returns {@code null} if such component cannot be found.
     *
     * @param clazz the class of component whose instance to be found.
     * @param container the container at which to begin the search
     * @param property the className of the bound property, exactly as expressed in
     * the accessor e.g. "Text" for getText(), "Value" for getValue().
     * @param value the value of the bound property
     * @param nested true to list components nested within another component
     * which is also an instance of <code>clazz</code>, false otherwise
     * @return the component, or null if no such component exists in the
     * container
     * @throws java.lang.IllegalArgumentException if the bound property does
     * not exist for the class or cannot be accessed
     */
    public static <T extends JComponent> T getDescendantOfType(Class<T> clazz,
                                                               Container container, String property, Object value, boolean nested)
            throws IllegalArgumentException {
        List<T> list = getDescendantsOfType(clazz, container, nested);
        return getComponentFromList(clazz, list, property, value);
    }

    /**
     * Convenience method for searching below <code>container</code> in the
     * component hierarchy and return nested components of class
     * <code>clazz</code> it finds.  Returns an empty list if no such
     * components exist in the container.
     * <P>
     * This method invokes getDescendantsOfClass(clazz, container, true)
     *
     * @param clazz the class of components to be found.
     * @param container the container at which to begin the search
     * @return the List of components
     */
    public static <T extends JComponent> List<T> getDescendantsOfClass(
            Class<T> clazz, Container container) {
        return getDescendantsOfClass(clazz, container, true);
    }

    /**
     * Convenience method for searching below <code>container</code> in the
     * component hierarchy and return nested components of class
     * <code>clazz</code> it finds.  Returns an empty list if no such
     * components exist in the container.
     *
     * @param clazz the class of components to be found.
     * @param container the container at which to begin the search
     * @param nested true to list components nested within another listed
     * component, false otherwise
     * @return the List of components
     */
    public static <T extends JComponent> List<T> getDescendantsOfClass(
            Class<T> clazz, Container container, boolean nested) {
        List<T> tList = new ArrayList<T>();
        for (Component component : container.getComponents()) {
            if (clazz.equals(component.getClass())) {
                tList.add(clazz.cast(component));
            }
            if (nested || !clazz.equals(component.getClass())) {
                tList.addAll(SwingUtils.<T>getDescendantsOfClass(clazz,
                        (Container) component, nested));
            }
        }
        return tList;
    }

    /**
     * Convenience method that searches below <code>container</code> in the
     * component hierarchy in a depth first manner and returns the first
     * found component of class <code>clazz</code> having the bound property
     * value.
     * <P>
     * Returns {@code null} if such component cannot be found.
     * <P>
     * This method invokes getDescendantOfClass(clazz, container, property,
     * value, true)
     *
     * @param clazz the class of component to be found.
     * @param container the container at which to begin the search
     * @param property the className of the bound property, exactly as expressed in
     * the accessor e.g. "Text" for getText(), "Value" for getValue().
     * This parameter is case sensitive.
     * @param value the value of the bound property
     * @return the component, or null if no such component exists in the
     * container's hierarchy.
     * @throws java.lang.IllegalArgumentException if the bound property does
     * not exist for the class or cannot be accessed
     */
    public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
                                                                Container container, String property, Object value)
            throws IllegalArgumentException {
        return getDescendantOfClass(clazz, container, property, value, true);
    }

    /**
     * Convenience method that searches below <code>container</code> in the
     * component hierarchy in a depth first manner and returns the first
     * found component of class <code>clazz</code> having the bound property
     * value.
     * <P>
     * Returns {@code null} if such component cannot be found.
     *
     * @param clazz the class of component to be found.
     * @param container the container at which to begin the search
     * @param property the className of the bound property, exactly as expressed
     * in the accessor e.g. "Text" for getText(), "Value" for getValue().
     * This parameter is case sensitive.
     * @param value the value of the bound property
     * @param nested true to include components nested within another listed
     * component, false otherwise
     * @return the component, or null if no such component exists in the
     * container's hierarchy
     * @throws java.lang.IllegalArgumentException if the bound property does
     * not exist for the class or cannot be accessed
     */
    public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
                                                                Container container, String property, Object value, boolean nested)
            throws IllegalArgumentException {
        List<T> list = getDescendantsOfClass(clazz, container, nested);
        return getComponentFromList(clazz, list, property, value);
    }

    private static <T extends JComponent> T getComponentFromList(Class<T> clazz,
                                                                 List<T> list, String property, Object value)
            throws IllegalArgumentException {
        T retVal = null;
        Method method = null;
        try {
            method = clazz.getMethod("get" + property);
        } catch (NoSuchMethodException ex) {
            try {
                method = clazz.getMethod("is" + property);
            } catch (NoSuchMethodException ex1) {
                throw new IllegalArgumentException("Property " + property +
                        " not found in class " + clazz.getName());
            }
        }
        try {
            for (T t : list) {
                Object testVal = method.invoke(t);
                if (equals(value, testVal)) {
                    return t;
                }
            }
        } catch (InvocationTargetException ex) {
            throw new IllegalArgumentException(
                    "Error accessing property " + property +
                            " in class " + clazz.getName());
        } catch (IllegalAccessException ex) {
            throw new IllegalArgumentException(
                    "Property " + property +
                            " cannot be accessed in class " + clazz.getName());
        } catch (SecurityException ex) {
            throw new IllegalArgumentException(
                    "Property " + property +
                            " cannot be accessed in class " + clazz.getName());
        }
        return retVal;
    }

    /**
     * Convenience method for determining whether two objects are either
     * equal or both null.
     *
     * @param obj1 the first reference object to compare.
     * @param obj2 the second reference object to compare.
     * @return true if obj1 and obj2 are equal or if both are null,
     * false otherwise
     */
    public static boolean equals(Object obj1, Object obj2) {
        return obj1 == null ? obj2 == null : obj1.equals(obj2);
    }

    /**
     * Convenience method for mapping a container in the hierarchy to its
     * contained components.  The keys are the containers, and the values
     * are lists of contained components.
     * <P>
     * Implementation note:  The returned value is a HashMap and the values
     * are of type ArrayList.  This is subject to change, so callers should
     * code against the interfaces Map and List.
     *
     * @param container The JComponent to be mapped
     * @param nested true to drill down to nested containers, false otherwise
     * @return the Map of the UI
     */
    public static Map<JComponent, List<JComponent>> getComponentMap(
            JComponent container, boolean nested) {
        HashMap<JComponent, List<JComponent>> retVal =
                new HashMap<JComponent, List<JComponent>>();
        for (JComponent component : getDescendantsOfType(JComponent.class,
                container, false)) {
            if (!retVal.containsKey(container)) {
                retVal.put(container,
                        new ArrayList<JComponent>());
            }
            retVal.get(container).add(component);
            if (nested) {
                retVal.putAll(getComponentMap(component, nested));
            }
        }
        return retVal;
    }

    /**
     * Convenience method for retrieving a subset of the UIDefaults pertaining
     * to a particular class.
     *
     * @param clazz the class of interest
     * @return the UIDefaults of the class
     */
    public static UIDefaults getUIDefaultsOfClass(Class clazz) {
        String name = clazz.getName();
        name = name.substring(name.lastIndexOf(".") + 2);
        return getUIDefaultsOfClass(name);
    }

    /**
     * Convenience method for retrieving a subset of the UIDefaults pertaining
     * to a particular class.
     *
     * @param className fully qualified name of the class of interest
     * @return the UIDefaults of the class named
     */
    public static UIDefaults getUIDefaultsOfClass(String className) {
        UIDefaults retVal = new UIDefaults();
        UIDefaults defaults = UIManager.getLookAndFeelDefaults();
        List<?> listKeys = Collections.list(defaults.keys());
        for (Object key : listKeys) {
            if (key instanceof String && ((String) key).startsWith(className)) {
                String stringKey = (String) key;
                String property = stringKey;
                if (stringKey.contains(".")) {
                    property = stringKey.substring(stringKey.indexOf(".") + 1);
                }
                retVal.put(property, defaults.get(key));
            }
        }
        return retVal;
    }

    /**
     * Convenience method for retrieving the UIDefault for a single property
     * of a particular class.
     *
     * @param clazz the class of interest
     * @param property the property to query
     * @return the UIDefault property, or null if not found
     */
    public static Object getUIDefaultOfClass(Class clazz, String property) {
        Object retVal = null;
        UIDefaults defaults = getUIDefaultsOfClass(clazz);
        List<Object> listKeys = Collections.list(defaults.keys());
        for (Object key : listKeys) {
            if (key.equals(property)) {
                return defaults.get(key);
            }
            if (key.toString().equalsIgnoreCase(property)) {
                retVal = defaults.get(key);
            }
        }
        return retVal;
    }

    /**
     * Exclude methods that return values that are meaningless to the user
     */
    static Set<String> setExclude = new HashSet<String>();
    static {
        setExclude.add("getFocusCycleRootAncestor");
        setExclude.add("getAccessibleContext");
        setExclude.add("getColorModel");
        setExclude.add("getGraphics");
        setExclude.add("getGraphicsConfiguration");
    }

    /**
     * Convenience method for obtaining most non-null human readable properties
     * of a JComponent.  Array properties are not included.
     * <P>
     * Implementation note:  The returned value is a HashMap.  This is subject
     * to change, so callers should code against the interface Map.
     *
     * @param component the component whose proerties are to be determined
     * @return the class and value of the properties
     */
    public static Map<Object, Object> getProperties(JComponent component) {
        Map<Object, Object> retVal = new HashMap<Object, Object>();
        Class<?> clazz = component.getClass();
        Method[] methods = clazz.getMethods();
        Object value = null;
        for (Method method : methods) {
            if (method.getName().matches("^(is|get).*") &&
                    method.getParameterTypes().length == 0) {
                try {
                    Class returnType = method.getReturnType();
                    if (returnType != void.class &&
                            !returnType.getName().startsWith("[") &&
                            !setExclude.contains(method.getName())) {
                        String key = method.getName();
                        value = method.invoke(component);
                        if (value != null && !(value instanceof Component)) {
                            retVal.put(key, value);
                        }
                    }
                    // ignore exceptions that arise if the property could not be accessed
                } catch (IllegalAccessException ex) {
                } catch (IllegalArgumentException ex) {
                } catch (InvocationTargetException ex) {
                }
            }
        }
        return retVal;
    }

    /**
     * Convenience method to obtain the Swing class from which this
     * component was directly or indirectly derived.
     *
     * @param component The component whose Swing superclass is to be
     * determined
     * @return The nearest Swing class in the inheritance tree
     */
    public static <T extends JComponent> Class getJClass(T component) {
        Class<?> clazz = component.getClass();
        while (!clazz.getName().matches("javax.swing.J[^.]*$")) {
            clazz = clazz.getSuperclass();
        }
        return clazz;
    }
}
